# -*- coding: utf-8 -*-
"""
Created on Tue Jul  9 09:26:24 2019

@author: MS
"""
import numpy as np

def AnalysisData(X,DataType=None):
    # 分析数据
    # DataType 返回每个属性的数据类型，若为字符串类型，说明是离散型，并统计各个可能取值
    # X1 返回数字化的数据，将离散型数值转变为相应取值的编号
    m=len(X)     #样本数
    n=len(X[0])  #属性数
    if DataType==None: #如果未从外部输入DataType,则需自动创建
        DataType=[]
        for p in range(n):
            DataType.append([type(X[0][p])])
            if DataType[p][0]==str:
                for q in range(m):
                    if X[q][p] not in DataType[p]:
                        DataType[p].append(X[q][p])
    X1=[]   #数字化的数据
    for p in range(m):
        temp=[]
        for q in range(n):
            if DataType[q][0]==str:
                temp.append(DataType[q].index(X[p][q]))
            else:
                temp.append(X[p][q])
        X1.append(temp)
    return DataType,X1

def OneHot(X,DataType,order):
    # one-hot独热编码，将原数据X变为独热编码的数据X1
    if order==None:
        order=[True]*len(DataType)
    num=[len(i)-1 for i in DataType]  #每个属性的取值数
    expand=[num[i]*(not order[i])+order[i] for i in range(len(num))] #每个属性需要扩展的数目
    X1=[]
    for i in range(len(num)):
        if expand[i]==1:
            X1=np.r_[X1,X[:,i]]
        else:
            for n in range(expand[i]):
                X1=np.r_[X1,(X[:,i]==(n+1))]
    return X1.reshape(sum(expand),-1).T

def sigmoid(X):
    return 1/(1+np.exp(-X))

def E(X,Y,para):
    # 计算当前参数下的累积误差
    m,d=X.shape
    m,l=Y.shape
    q=int((len(para)-l)/(d+l+1))             #隐层单元数目
    V,W,gama,theta=extract_para(para,d,q,l)  #从扩展的参数中解构出各个参数
    b=sigmoid(np.dot(X,V)-np.dot(np.ones([m,1]),gama))
    Ypre=sigmoid(np.dot(b,W)-np.dot(np.ones([m,1]),theta))
    return np.linalg.norm(Ypre-Y)**2/2/m

def predict(X,para):
    # 通过神经网络模型进行预测
    V,W,gama,theta=para
    m,d=X.shape
    b=sigmoid(np.dot(X,V)-np.dot(np.ones([m,1]),gama))        # 隐藏层输出
    Ypre=sigmoid(np.dot(b,W)-np.dot(np.ones([m,1]),theta))  # 输出层输出
    return Ypre

def initialpara(d,q,l):
    # 初始化参数
    V=np.random.randn(d,q)     # “输入层-隐藏层”连接权重, 产生正则分布的随机数
    W=np.random.randn(q,l)     # “隐藏层-输出层”连接权重
    gama=np.random.randn(1,q)  # 隐藏层阈值
    theta=np.random.randn(1,l) #输出层阈值
    return (V,W,gama,theta)

def expand_para(para):
    # 将多个参数扩展为向量
    V,W,gama,theta=para
    return np.r_[V.reshape(-1),W.reshape(-1),gama.reshape(-1),theta.reshape(-1)]

def extract_para(exp_para,d,q,l):
    # 从扩展后的单个向量中分解为出各个权值和阈值参数
    V=exp_para[:d*q].reshape(d,-1)
    W=exp_para[d*q:q*(d+l)].reshape(q,-1)
    theta=exp_para[-l:].reshape(1,-1)
    gama=exp_para[-(q+l):-l].reshape(1,-1)
    return V,W,gama,theta

def grad(X,Y,para):
    # BP法计算梯度
    # para为扩展形式的参数,维度 (..,1)
    m,d=X.shape
    m,l=Y.shape
    q=int((len(para)-l)/(d+l+1))             #隐层单元数目
    V,W,gama,theta=extract_para(para,d,q,l)  #从扩展的参数中解构出各个参数 
    #--------前向传播--------
    b=sigmoid(np.dot(X,V)-np.dot(np.ones([m,1]),gama))      # 隐藏层输出
    Ypre=sigmoid(np.dot(b,W)-np.dot(np.ones([m,1]),theta))  # 输出层输出
    #--------后向传播--------
    g=Ypre*(1-Ypre)*(Y-Ypre)
    e=b*(1-b)*(np.dot(g,W.T))
    #--------计算梯度--------
    dW=-np.dot(b.T,g)/m
    dV=-np.dot(X.T,e)/m
    dtheta=np.dot(np.ones([1,m]),g)/m
    dgama=np.dot(np.ones([1,m]),e)/m
    return expand_para((dV,dW,dgama,dtheta))

def BP(X,Y,q,para0=None,num=None,alpha=0.1,iteration=10):
    # 固定学习率的BP算法 

    # 数据X，Y均为np.array类型
    # q-----------隐层神经元数
    # para--------初始参数
    # num---------训练样本数(默认取值None表示批梯度下降，取值1则为随机梯度下降，其余数值为小批量)
    # alpha-------学习率

    # iteration---迭代次数
    #=======================================
    m,d=X.shape   #m为样本数，d为特征数
    m,l=Y.shape   #l为输出向量维数
    #===============初始化参数===============
    if para0==None:
        para0=initialpara(d,q,l)
    V0,W0,gama0,theta0=para0
    V=V0.copy()            # “输入层-隐藏层”连接权重, 产生正则分布的随机数
    W=W0.copy()            # “隐藏层-输出层”连接权重
    gama=gama0.copy()      # 隐藏层阈值
    theta=theta0.copy()    #输出层阈值
    para=expand_para((V,W,gama,theta))
    #==============训练神经网络==============
    v=np.zeros(d*q+q*l+q+l)
    erro=[]
    for i in range(iteration):
        if num==None:
            ks=np.arange(m)
        elif num==1:
            ks=[np.random.randint(m)]
        else:
            ks=np.random.permutation(m)[:num]
        g=grad(X[ks,:],Y[ks,:],para)
        para=para-alpha*g
        #---计算当前累积均方误差--
        erro.append(E(X,Y,para))
    return extract_para(para,d,q,l),erro

def BP_Linear(X,Y,q,para0=None,num=None,alpha1=0.3,alpha2=0.1,tmax=500,iteration=10):
    # BP_Linear算法
    # 学习率从alpha1线性变化至alpha2，tmax次迭代后不再变化
    
    # 数据X，Y均为np.array类型
    # q-----------隐层神经元数
    # para--------初始参数
    # num---------训练样本数(默认取值None表示批梯度下降，取值1则为随机梯度下降，其余数值为小批量)
    # alpha1------初始学习率
    # alpha2------终了学习率
    # tmax--------学习率停止变化的迭代次数
    # iteration---迭代次数
    #=======================================
    m,d=X.shape   #m为样本数，d为特征数
    m,l=Y.shape   #l为输出向量维数
    alpha=np.r_[alpha1*np.ones(d*q),alpha2*np.ones(q*l),alpha1*np.ones(q),alpha2*np.ones(l)]
    #===============初始化参数===============
    if para0==None:
        para0=initialpara(d,q,l)
    V0,W0,gama0,theta0=para0
    V=V0.copy()            # “输入层-隐藏层”连接权重, 产生正则分布的随机数
    W=W0.copy()            # “隐藏层-输出层”连接权重
    gama=gama0.copy()      # 隐藏层阈值
    theta=theta0.copy()    #输出层阈值
    para=expand_para((V,W,gama,theta))
    #==============训练神经网络==============
    erro=[]
    for i in range(iteration):
        if num==None:
            ks=np.arange(m)
        elif num==1:
            ks=[np.random.randint(m)]
        else:
            ks=np.random.permutation(m)[:num]
        g=grad(X[ks,:],Y[ks,:],para)
        if i>=tmax:
            a=alpha2
        else:
            a=(1-i/tmax)*alpha1+i/tmax*alpha2
        para=para-a*g
        #---计算当前累积均方误差--
        erro.append(E(X,Y,para))
    return extract_para(para,d,q,l),erro